package br.net.woodstock.tickettoride.api.manager.common;

import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.crypto.SecretKey;

import br.net.woodstock.rockframework.domain.service.ServiceException;
import br.net.woodstock.rockframework.persistence.orm.QueryMetadata;
import br.net.woodstock.rockframework.util.DateBuilder;
import br.net.woodstock.tickettoride.api.core.common.AbstractCommonService;
import br.net.woodstock.tickettoride.api.core.utils.RepositoryHelper;
import br.net.woodstock.tickettoride.api.manager.TicketService;
import br.net.woodstock.tickettoride.client.utils.CodecUtils;
import br.net.woodstock.tickettoride.client.utils.KeyUtils;
import br.net.woodstock.tickettoride.orm.Application;
import br.net.woodstock.tickettoride.orm.ApplicationStatus;
import br.net.woodstock.tickettoride.orm.Session;
import br.net.woodstock.tickettoride.orm.Ticket;
import br.net.woodstock.tickettoride.orm.TicketDestination;
import br.net.woodstock.tickettoride.orm.TicketStatus;

public abstract class AbstractTicketService extends AbstractCommonService implements TicketService {

	private static final long	serialVersionUID								= 1L;

	private static final int	DEFAULT_TIME									= 5;

	private static final int	KEY_SIZE										= 64;

	private static final String	JPQL_GET_TICKET_BY_HASH							= "SELECT t FROM Ticket AS t WHERE t.hash = :hash";

	private static final String	JPQL_LIST_TICKET_BY_APPLICATION					= "SELECT t FROM Ticket AS t JOIN t.application AS at JOIN t.session AS s JOIN s.certificate AS c JOIN c.application AS af WHERE (:fid = 0 OR af.id = :fid) AND (:tid = 0 OR at.id = :tid) ORDER BY t.creation DESC";

	private static final String	JPQL_LIST_PERMISSION_BY_DESTINATION_APPLICATION	= "SELECT s.id FROM ApplicationPermission AS p JOIN p.destination AS d JOIN p.source AS s WHERE d.id = :id";

	private static final String	JPQL_GET_TICKET_DESTINATION_BY_ID_APP			= "SELECT td FROM TicketDestination AS td JOIN td.ticket AS t JOIN td.application AS a WHERE t.id = :ticketId AND a.id = :applicationId";

	@Override
	public Ticket getById(final Integer id) {
		try {
			return this.getSimpleRepository().get(new Ticket(id));
		} catch (Exception e) {
			throw new ServiceException(e);
		}
	}

	@Override
	public Ticket getByHash(final String hash) {
		try {
			Map<String, Object> parameters = new HashMap<String, Object>();
			parameters.put("hash", hash);
			QueryMetadata metadata = RepositoryHelper.toQueryMetadata(AbstractTicketService.JPQL_GET_TICKET_BY_HASH, parameters);
			return this.getSimpleQueryableRepository().getSingle(metadata);
		} catch (Exception e) {
			throw new ServiceException(e);
		}
	}

	@Override
	public Integer create(final Integer sessionId, final Integer[] applicationIds, final byte[] data, final Date expires) {
		try {
			Session session = this.getSimpleRepository().get(new Session(sessionId));

			if (session == null) {
				throw new ServiceException("Application not found " + sessionId);
			}

			Map<String, Object> parameters = new HashMap<String, Object>();
			for (Integer applicationId : applicationIds) {
				Application application = this.getSimpleRepository().get(new Application(applicationId));

				if (application != null) {
					if (application.getStatus() == ApplicationStatus.ACTIVE) {
						boolean granted = false;
						parameters.put("id", applicationId);
						QueryMetadata metadata = RepositoryHelper.toQueryMetadata(AbstractTicketService.JPQL_LIST_PERMISSION_BY_DESTINATION_APPLICATION, parameters);
						Collection<Integer> ids = this.getSimpleQueryableRepository().getCollection(metadata).getResult();
						for (Integer id : ids) {
							if (id.equals(session.getCertificate().getApplication().getId())) {
								granted = true;
								break;
							}
						}
						if (!granted) {
							throw new ServiceException("Cannot create ticket from " + session.getCertificate().getApplication().getAlias() + " to " + application.getAlias());
						}
					} else {
						throw new ServiceException("Application isn't active " + application.getAlias());
					}
				} else {
					throw new ServiceException("Application not found " + applicationId);
				}

			}

			Date date = expires;
			if (date == null) {
				DateBuilder dateBuilder = new DateBuilder();
				dateBuilder.addMinutes(AbstractTicketService.DEFAULT_TIME);
				date = dateBuilder.getDate();
			}

			String hash = CodecUtils.getRandomString(AbstractTicketService.KEY_SIZE);

			Ticket ticket = new Ticket();
			ticket.setCreation(new Date());
			ticket.setData(data);
			ticket.setExpires(date);
			ticket.setHash(hash);
			ticket.setSession(new Session(sessionId));
			this.getSimpleRepository().save(ticket);

			for (Integer applicationId : applicationIds) {
				TicketDestination destination = new TicketDestination();
				destination.setApplication(new Application(applicationId));
				destination.setStatus(TicketStatus.ACTIVE);
				destination.setTicket(ticket);
				this.getSimpleRepository().save(destination);
			}

			return ticket.getId();
		} catch (Exception e) {
			throw new ServiceException(e);
		}
	}

	@Override
	public byte[] getEncryptedTicketData(final Integer sessionId, final Integer ticketId) {
		try {
			Date date = new Date();

			Session dstSession = this.getSimpleRepository().get(new Session(sessionId));

			if (dstSession == null) {
				throw new ServiceException("Session not found " + sessionId);
			}

			Application dstApplicaction = dstSession.getCertificate().getApplication();

			if (dstApplicaction.getStatus() != ApplicationStatus.ACTIVE) {
				throw new ServiceException("Application isn't active " + dstApplicaction.getAlias());
			}

			Ticket ticket = this.getSimpleRepository().get(new Ticket(ticketId));
			if (ticket.getExpires().compareTo(date) < 0) {
				throw new ServiceException("Ticket expired");
			}

			SecretKey dstKey = KeyUtils.loadSecretKey(CodecUtils.fromBase64(dstSession.getKey()));
			Session srcSession = ticket.getSession();
			SecretKey srcKey = KeyUtils.loadSecretKey(CodecUtils.fromBase64(srcSession.getKey()));

			Map<String, Object> parameters = new HashMap<String, Object>();
			parameters.put("ticketId", ticket.getId());
			parameters.put("applicationId", dstApplicaction.getId());

			QueryMetadata metadata = RepositoryHelper.toQueryMetadata(AbstractTicketService.JPQL_GET_TICKET_DESTINATION_BY_ID_APP, parameters);

			TicketDestination destination = this.getSimpleQueryableRepository().getSingle(metadata);

			if (destination.getStatus() != TicketStatus.ACTIVE) {
				throw new ServiceException("Ticket cannot be read again");
			}

			if (!dstApplicaction.getId().equals(destination.getApplication().getId())) {
				throw new ServiceException("Application cannot read ticket");
			}

			byte[] data = CodecUtils.fromBase64(ticket.getData());
			byte[] decrypted = CodecUtils.decrypt(srcKey, data);
			byte[] encrypted = CodecUtils.encrypt(dstKey, decrypted);

			destination.setStatus(TicketStatus.INACTIVE);
			this.getSimpleRepository().update(destination);

			return encrypted;
		} catch (Exception e) {
			throw new ServiceException(e);
		}
	}

	@Override
	public String getPlainTicketData(final Integer id) {
		try {
			Ticket ticket = this.getSimpleRepository().get(new Ticket(id));
			Session srcSession = ticket.getSession();
			SecretKey srcKey = KeyUtils.loadSecretKey(CodecUtils.fromBase64(srcSession.getKey()));

			byte[] data = CodecUtils.fromBase64(ticket.getData());
			byte[] decrypted = CodecUtils.decrypt(srcKey, data);

			return CodecUtils.toString(decrypted);
		} catch (Exception e) {
			throw new ServiceException(e);
		}
	}

	@Override
	public Collection<Ticket> listByApplication(final Integer fromApplicationId, final Integer toApplicationId) {
		try {
			Integer fid = fromApplicationId;
			Integer tid = toApplicationId;
			if (fid == null) {
				fid = Integer.valueOf(0);
			}
			if (tid == null) {
				tid = Integer.valueOf(0);
			}
			Map<String, Object> parameters = new HashMap<String, Object>();
			parameters.put("fid", fid);
			parameters.put("tid", tid);
			QueryMetadata metadata = RepositoryHelper.toQueryMetadata(AbstractTicketService.JPQL_LIST_TICKET_BY_APPLICATION, parameters);
			return this.getSimpleQueryableRepository().getCollection(metadata).getResult();
		} catch (Exception e) {
			throw new ServiceException(e);
		}
	}

}
